#include "DocumentStreamReader.h"

#include "Document.h"
#include "IDocumentObject.h"

#include <QDebug>
#include <QMetaObject>
#include <QMetaProperty>
#include <QMetaType>
#include <QVariant>
#include <QXmlStreamReader>

#include <QPointF>
#include <QSizeF>

using namespace LDO;

#define DEBUG() qDebug() << __PRETTY_FUNCTION__

DocumentStreamReader::DocumentStreamReader(QObject *parent)
    : QObject(parent)
    , m_reader(new QXmlStreamReader)
{
}

void DocumentStreamReader::setDevice(QIODevice *device)
{
    m_reader->setDevice(device);
}

QIODevice *DocumentStreamReader::device() const
{
    return m_reader->device();
}

QString DocumentStreamReader::errorString() const
{
    return QString("At character %3 (line %1, column %2):\n%4")
            .arg(m_reader->lineNumber())
            .arg(m_reader->columnNumber())
            .arg(m_reader->characterOffset())
            .arg(m_reader->errorString());
}

bool DocumentStreamReader::readDocument(Document *document)
{
    if (document == nullptr)
    {
        raiseError(QString("readDocument: Cannot read into a null document"));
        return false;
    }

    m_document = document;

    if (!readNextStartElement())
    {
        raiseError(QString("readDocument: Malformed XML document"));
        return false;
    }
    if (!isNamed("document"))
    {
        raiseError(QString("readDocument: Malformed document: expecting 'document', got '%1'")
                   .arg(m_reader->name().toString()));
        return false;
    }

    while (readNextStartElement())
    {
        if (!isNamed("object"))
        {
            raiseError(QString("readDocument: Malformed document: expecting 'object', got '%1'")
                       .arg(m_reader->name().toString()));
            skipCurrentElement();
            continue;
        }
        if (!hasAttribute("class"))
        {
            raiseError(QString("readDocument: Malformed document: 'object' has no 'class' attribute"));
            skipCurrentElement();
            continue;
        }
        const QString classPointerName = m_reader->attributes().value(QStringLiteral("class")).toString();
        IDocumentObject *object = readObjectStarValue(classPointerName).value<IDocumentObject*>();
        if (object == nullptr)
        {
            raiseError(QString("readDocument: Got invalid object of class '%1'\n%2")
                       .arg(classPointerName)
                       .arg(m_reader->errorString()));
            continue;
        }
        else
            document->addChildObject(object);
    }

    return !m_reader->hasError();
}

IDocumentObject *DocumentStreamReader::readObject(const QMetaObject &metaObject)
{
    // TODO: only check if we can create the object
    // Get the metaObject without creating it.
    // Create object and set all properties at the end.
    IDocumentObject *object = m_document->createObject(metaObject.className());
    if (object == nullptr)
    {
        raiseWarning(QString("readObject: Unsupported document object: \"%1\"").arg(metaObject.className()));
        return nullptr;
    }

    // Store meta-property by name, for convenience
    QMap<QString, QMetaProperty> metaPropertyMap;
    for (int i = 0; i<metaObject.propertyCount(); i++)
    {
        QMetaProperty metaProperty = metaObject.property(i);
        metaPropertyMap.insert(QString(metaProperty.name()), metaProperty);
    }

    // Read and store property value by name, for convenience
    QMap<QString, QVariant> propertyMap;
    while (readNextStartElement())
    {
        if (!isNamed("property"))
        {
            raiseWarning(QString("readObject: Unexpected XML tag: \"%1\"").arg(m_reader->name().toString()));
            skipCurrentElement();
            continue;
        }
        if (!hasAttribute("name"))
        {
            raiseWarning(QString("readObject: XML property has no name"));
            skipCurrentElement();
            continue;
        }
        const QString xmlPropertyName = m_reader->attributes().value("name").toString();
        if (!hasAttribute("type"))
        {
            raiseWarning(QString("readObject: XML property has no type"));
            skipCurrentElement();
            continue;
        }
        const QString xmlPropertyTypeName = m_reader->attributes().value("type").toString();
        if (!metaPropertyMap.contains(xmlPropertyName))
        {
            raiseWarning(QString("readObject: Unknown property name: \"%1\"").arg(xmlPropertyName));
            skipCurrentElement();
            continue;
        }
        QMetaProperty metaProperty = metaPropertyMap.value(xmlPropertyName);
        if (xmlPropertyTypeName != metaProperty.typeName() &&
                !isDerivedFrom(xmlPropertyTypeName, metaProperty.typeName()))
        {
            raiseWarning(QString("readObject: XML property type and object property meta-type mismatch: \"%1\" vs \"%2\" for property %3::%4")
                         .arg(xmlPropertyTypeName, metaProperty.typeName(), metaObject.className(), metaProperty.name()));
            skipCurrentElement();
            continue;
        }
        const QVariant value = readPropertyValue(metaProperty, xmlPropertyTypeName);
        if (!value.isValid())
        {
            raiseWarning(QString("readObject: Got an invalid value for property %1::%2\n%3")
                         .arg(metaObject.className(), metaProperty.name(), m_reader->errorString()));
            continue;
        }
        if (!object->setProperty(xmlPropertyName.toUtf8().constData(), value))
        {
            raiseWarning(QString("readObject: Failed to set property %1::%2")
                         .arg(metaObject.className(), metaProperty.name()));
            continue;
        }
    }

    if (m_reader->hasError())
    {
        delete object;
        return nullptr;
    }

    return object;
}

QVariant DocumentStreamReader::readPropertyValue(const QMetaProperty &metaProperty, const QString &wantedTypeName)
{
    switch (metaProperty.type())
    {
        case QVariant::Bool:
            return readBoolValue();
        case QVariant::Int:
            return readIntValue();
//        case QVariant::UInt:
//            break;
//        case QVariant::LongLong:
//            break;
//        case QVariant::ULongLong:
//            break;
        case QVariant::Double:
            return readDoubleValue();
//        case QVariant::Char:
//            break;
//        case QVariant::Map:
//            break;
//        case QVariant::List:
//            break;
        case QVariant::String:
            return readStringValue();
        case QVariant::StringList:
            return readStringListValue();
//        case QVariant::ByteArray:
//            break;
//        case QVariant::BitArray:
//            break;
//        case QVariant::Date:
//            break;
//        case QVariant::Time:
//            break;
//        case QVariant::DateTime:
//            break;
//        case QVariant::Url:
//            break;
//        case QVariant::Locale:
//            break;
//        case QVariant::Rect:
//            break;
//        case QVariant::RectF:
//            break;
//        case QVariant::Size:
//            break;
        case QVariant::SizeF:
            return readSizeValue();
//        case QVariant::Line:
//            break;
//        case QVariant::LineF:
//            break;
//        case QVariant::Point:
//            break;
        case QVariant::PointF:
            return readPointValue();
//        case QVariant::RegExp:
//            break;
//        case QVariant::RegularExpression:
//            break;
//        case QVariant::Hash:
//            break;
//        case QVariant::EasingCurve:
//            break;
//        case QVariant::Uuid:
//            break;
//        case QVariant::ModelIndex:
//            break;
//        case QVariant::PersistentModelIndex:
//            break;
//        case QVariant::Font:
//            break;
//        case QVariant::Pixmap:
//            break;
//        case QVariant::Brush:
//            break;
//        case QVariant::Color:
//            break;
//        case QVariant::Palette:
//            break;
//        case QVariant::Image:
//            break;
//        case QVariant::Polygon:
//            break;
//        case QVariant::Region:
//            break;
//        case QVariant::Bitmap:
//            break;
//        case QVariant::Cursor:
//            break;
//        case QVariant::KeySequence:
//            break;
//        case QVariant::Pen:
//            break;
//        case QVariant::TextLength:
//            break;
//        case QVariant::TextFormat:
//            break;
//        case QVariant::Matrix:
//            break;
//        case QVariant::Transform:
//            break;
//        case QVariant::Matrix4x4:
//            break;
//        case QVariant::Vector2D:
//            break;
//        case QVariant::Vector3D:
//            break;
//        case QVariant::Vector4D:
//            break;
//        case QVariant::Quaternion:
//            break;
//        case QVariant::PolygonF:
//            break;
//        case QVariant::Icon:
//            break;
//        case QVariant::SizePolicy:
//            break;
        case QVariant::UserType:
        {
            const int typeId = metaProperty.userType();
            const QString metaTypeName = QMetaType::typeName(typeId);
            QMetaType metaType(typeId);
            if (metaType.flags() & QMetaType::IsEnumeration)
            {
                if (metaProperty.enumerator().isFlag())
                    return readFlagValue(typeId, metaProperty.enumerator());
                else
                    return readEnumValue(typeId, metaProperty.enumerator());
            }
            else if (metaType.flags() & QMetaType::PointerToQObject)
            {
                // Allow to ask for DerivedClass* to set a BaseClass* property
                if (wantedTypeName.isEmpty())
                    return readObjectStarValue(metaTypeName);
                else
                    return readObjectStarValue(wantedTypeName);
            }
            else if (metaProperty.enumerator().isFlag())
            {
                return readFlagValue(typeId, metaProperty.enumerator());
            }
            else if (metaTypeName.startsWith("QList<"))
            {
                QString containedTypeName = metaTypeName.mid(6, metaTypeName.count() - 6 - 1);
                return readListOf(containedTypeName);
            }
            else
            {
                raiseWarning(QString("readPropertyValue: Unsupported user meta-type: \"%1\"").arg(metaTypeName));
                skipCurrentElement();
                return QVariant();
            }
        }
        case QVariant::Invalid:
            raiseWarning(QString("readPropertyValue: Invalid meta-type"));
            skipCurrentElement();
            return QVariant();
        default:
            raiseWarning(QString("readPropertyValue: Unsupported meta-type: \"%1\"").arg(metaProperty.typeName()));
            skipCurrentElement();
            return QVariant();
    }
}

QVariant DocumentStreamReader::readIntValue()
{
    const QString text = readElementText();
    if (m_reader->hasError())
        return QVariant();
    QVariant variant(text);
    if (variant.convert(QVariant::Int))
        return variant;
    return QVariant();
}

QVariant DocumentStreamReader::readDoubleValue()
{
    const QString text = readElementText();
    if (m_reader->hasError())
        return QVariant();
    QVariant variant(text);
    if (variant.convert(QVariant::Double))
        return variant;
    return QVariant();
}

QVariant DocumentStreamReader::readBoolValue()
{
    const QString text = readElementText();
    if (m_reader->hasError())
        return QVariant();
    QVariant variant(text);
    if (variant.convert(QVariant::Bool))
        return variant;
    return QVariant();
}

QVariant DocumentStreamReader::readStringValue()
{
    const QString text = readElementText();
    if (m_reader->hasError())
        return QVariant();
    QVariant variant(text);
    if (variant.convert(QVariant::String))
        return variant;
    return QVariant();
}

QVariant DocumentStreamReader::readStringListValue()
{
    QStringList result;
    while (readNextStartElement())
    {
        if (!isNamed("string"))
        {
            skipCurrentElement();
            continue;
        }
        QVariant value = readStringValue();
        if (!value.isValid())
        {
            continue;
        }
        result.append(value.toString());
    }
    return QVariant::fromValue(result);
}

QVariant DocumentStreamReader::readPointValue()
{
    if (!readNextStartElement())
    {
        return QVariant();
    }
    if (!isNamed("x"))
    {
        skipCurrentElement();
        return QVariant();
    }
    QVariant x = readDoubleValue();
    if (!x.isValid())
    {
        skipCurrentElement();
        return QVariant();
    }

    if (!readNextStartElement())
    {
        return QVariant();
    }
    if (!isNamed("y"))
    {
        skipCurrentElement();
        return QVariant();
    }
    QVariant y = readDoubleValue();
    if (!y.isValid())
    {
        return QVariant();
    }

    if (readNextStartElement())
    {
        skipCurrentElement();
        return QVariant();
    }

    return QVariant::fromValue(QPointF(x.toDouble(), y.toDouble()));
}

QVariant DocumentStreamReader::readSizeValue()
{
    if (!readNextStartElement())
    {
        return QVariant();
    }
    if (!isNamed("width"))
    {
        skipCurrentElement();
        return QVariant();
    }
    QVariant width = readDoubleValue();
    if (!width.isValid())
    {
        return QVariant();
    }

    if (!readNextStartElement())
    {
        return QVariant();
    }
    if (!isNamed("height"))
    {
        skipCurrentElement();
        return QVariant();
    }
    QVariant height = readDoubleValue();
    if (!height.isValid())
    {
        return QVariant();
    }

    if (readNextStartElement())
    {
        skipCurrentElement();
        return QVariant();
    }

    return QVariant::fromValue(QSizeF(width.toDouble(), height.toDouble()));
}

QVariant DocumentStreamReader::readEnumValue(int typeId, const QMetaEnum &metaEnum)
{
    const QString text = readElementText();
    if (m_reader->hasError())
        return QVariant();

    // TBD: QVariant is fine with enum literal <-> string
    Q_UNUSED(metaEnum)
    QVariant variant(text);
    if (!variant.convert(typeId))
    {
        raiseError(QString("readEnumValue: Invalid enum value: \"%1\"").arg(text));
        return QVariant();
    }
    return variant;
}

QVariant DocumentStreamReader::readFlagValue(int typeId, const QMetaEnum &metaEnum)
{
    const QString text = readElementText();
    if (m_reader->hasError())
        return QVariant();
    int value = 0;
    QStringList tokens = text.split("|", QString::SkipEmptyParts);
    for (const QString &token: tokens)
    {
        bool ok;
        int flag = metaEnum.keysToValue(token.toUtf8().constData(), &ok);
        if (!ok)
        {
            raiseWarning(QString("readFlagValue: Ignoring invalid flag value '%1' for type '%2'")
                         .arg(token, metaEnum.name()));
            continue;
        }
        value |= flag;
    }
    return QVariant(typeId, &value);
}

QVariant DocumentStreamReader::readListOfIntValue()
{
    QList<int> result;
    while (readNextStartElement())
    {
        if (!isNamed("item"))
        {
            skipCurrentElement();
            continue;
        }
        QVariant value = readIntValue();
        if (!value.isValid())
        {
            continue;
        }
        result.append(value.value<int>());
    }
    return QVariant::fromValue(result);
}

QVariant DocumentStreamReader::readListOfDoubleValue()
{
    QList<double> result;
    while (readNextStartElement())
    {
        if (!isNamed("item"))
        {
            skipCurrentElement();
            continue;
        }
        QVariant value = readDoubleValue();
        if (!value.isValid())
        {
            continue;
        }
        result.append(value.value<double>());
    }
    return QVariant::fromValue(result);
}

QVariant DocumentStreamReader::readListOfBoolValue()
{
    QList<bool> result;
    while (readNextStartElement())
    {
        if (!isNamed("item"))
        {
            skipCurrentElement();
            continue;
        }
        QVariant value = readBoolValue();
        if (!value.isValid())
        {
            continue;
        }
        result.append(value.value<bool>());
    }
    return QVariant::fromValue(result);
}

QVariant DocumentStreamReader::readListOfStringValue()
{
    QList<QString> result;
    while (readNextStartElement())
    {
        if (!isNamed("item"))
        {
            skipCurrentElement();
            continue;
        }
        QVariant value = readStringValue();
        if (!value.isValid())
        {
            continue;
        }
        result.append(value.value<QString>());
    }
    return QVariant::fromValue(result);
}

QVariant DocumentStreamReader::readListOfPointValue()
{
    QList<QPointF> result;
    while (readNextStartElement())
    {
        if (!isNamed("item"))
        {
            skipCurrentElement();
            continue;
        }
        QVariant value = readPointValue();
        if (!value.isValid())
        {
            continue;
        }
        result.append(value.value<QPointF>());
    }
    return QVariant::fromValue(result);
}

QVariant DocumentStreamReader::readListOfSizeValue()
{
    QList<QSizeF> result;
    while (readNextStartElement())
    {
        if (!isNamed("item"))
        {
            skipCurrentElement();
            continue;
        }
        QVariant value = readSizeValue();
        if (!value.isValid())
        {
            continue;
        }
        result.append(value.value<QSizeF>());
    }
    return QVariant::fromValue(result);
}

QVariant DocumentStreamReader::readListOfStringListValue()
{
    QList<QStringList> result;
    while (readNextStartElement())
    {
        if (!isNamed("item"))
        {
            skipCurrentElement();
            continue;
        }
        QVariant value = readStringListValue();
        if (!value.isValid())
        {
            continue;
        }
        result.append(value.value<QStringList>());
    }
    return QVariant::fromValue(result);
}

QVariant DocumentStreamReader::readListOfObjectStarValue(const QString &classPointerTypeName)
{
    QList<QObject*> result;
    while (readNextStartElement())
    {
        if (!isNamed("item"))
        {
            skipCurrentElement();
            continue;
        }
        QVariant value = readObjectStarValue(classPointerTypeName);
        if (!value.isValid())
        {
            raiseWarning(QString("readListOfObjectStarValue: Got an invalid '%1' value\n%2")
                         .arg(classPointerTypeName, m_reader->errorString()));
            continue;
        }
        if (!(value.canConvert(QMetaType::QObjectStar) &&
                value.convert(QMetaType::QObjectStar)))
        {
            raiseWarning(QString("readListOfObjectStarValue: Cannot convert QVariant form \"%1\" to \"QObject*\"")
                         .arg(classPointerTypeName));
            continue;
        }
        result.append(value.value<QObject*>());
    }

    const QByteArray desiredMetaTypeName = QString("QList<%1>").arg(classPointerTypeName).toUtf8();
    const int desiredMetaTypeId = QMetaType::type(desiredMetaTypeName);
    return QVariant(desiredMetaTypeId, &result);
}

QVariant DocumentStreamReader::readListOf(const QString &typeName)
{
    if (typeName == "QPointF")
        return readListOfPointValue();
    if (typeName == "QSizeF")
        return readListOfSizeValue();
    if (typeName == "QString")
        return readListOfStringValue();
    if (typeName == "QStringList")
        return readListOfStringListValue();
    if (typeName == "bool")
        return readListOfBoolValue();
    if (typeName == "double")
        return readListOfDoubleValue();
    if (typeName == "int")
        return readListOfIntValue();
    if (typeName.endsWith("*"))
        return readListOfObjectStarValue(typeName);

    raiseWarning(QString("readListOf: Unhandled type name: \"%1\"").arg(typeName));
    skipCurrentElement();
    return QVariant();
}

QVariant DocumentStreamReader::readObjectStarValue(const QString &typeName)
{
    const int metaTypeId = QMetaType::type(typeName.toUtf8()) ;
    if (metaTypeId == QMetaType::UnknownType)
    {
        raiseWarning(QString("readObjectStarValue: Unknown meta-type name: \"%1\"").arg(typeName));
        skipCurrentElement();
        return QVariant();
    }
    const QMetaType metaType(metaTypeId);
    IDocumentObject *object = readObject(*metaType.metaObject());
    if (object == nullptr)
    {
        raiseWarning(QString("readObjectStarValue: Failed to read object of type \"%1\"\n%2")
                     .arg(typeName, m_reader->errorString()));
        return QVariant();
    }

    // metaTypeId is the id for "pointer to object of class 'typeName'"
    return QVariant(metaTypeId, &object);
}

bool DocumentStreamReader::strictMode() const
{
    return m_strictMode;
}

bool DocumentStreamReader::debugEnabled() const
{
    return m_debugEnabled;
}

void DocumentStreamReader::setStrictMode(bool strict)
{
    if (m_strictMode == strict)
        return;

    m_strictMode = strict;
    emit strictModeChanged(strict);
}

void DocumentStreamReader::setDebugEnabled(bool enabled)
{
    if (m_debugEnabled == enabled)
        return;

    m_debugEnabled = enabled;
    emit debugEnabledChanged(enabled);
}

bool DocumentStreamReader::readNextStartElement()
{
    bool result = m_reader->readNextStartElement();
    if (m_debugEnabled)
        qDebug() << "readNextStartElement: " << result << m_reader->name().toString();
    return result;
}

QString DocumentStreamReader::readElementText()
{
    QString result = m_reader->readElementText();
    if (m_debugEnabled)
        qDebug() << "readElementText: " << m_reader->hasError() << result;
    return result;
}

bool DocumentStreamReader::isNamed(const QString &name)
{
    if (m_debugEnabled)
        qDebug() << "isNamed: " << m_reader->name() << name;
    return m_reader->name().compare(name) == 0;
}

bool DocumentStreamReader::hasAttribute(const QString &name)
{
    if (m_debugEnabled)
        qDebug() << "hasAttribute: " << m_reader->name() << m_reader->attributes().hasAttribute(name);
    return m_reader->attributes().hasAttribute(name);
}

void DocumentStreamReader::skipCurrentElement()
{
    if (m_debugEnabled)
        qDebug() << "skipCurrentElement: " << m_reader->name().toString();
    m_reader->skipCurrentElement();
}

void DocumentStreamReader::raiseWarning(const QString &message)
{
    if (m_strictMode)
    {
        raiseError(message);
    }
    else
        qWarning() << message;
}

void DocumentStreamReader::raiseError(const QString &message)
{
    qCritical() << message;
    m_reader->raiseError(message);
}

bool DocumentStreamReader::isDerivedFrom(const QString &pointerTypeName, const QString &superPointerTypeName)
{
    if (!pointerTypeName.endsWith("*") || !superPointerTypeName.endsWith("*"))
    {
//        qWarning() << "isDerivedFrom: cannot infer class hierarchy of non-pointer typename:"
//                   << pointerTypeName << "and" << superPointerTypeName;
        return false;
    }
    // QMetaObject::normalizedType();
    //const QString className(pointerTypeName.left(pointerTypeName.count() - 1));
    const int metaTypeId = QMetaType::type(pointerTypeName.toUtf8());
    const QMetaType metaType(metaTypeId);
    //const QString superClassName(superPointerTypeName.left(superPointerTypeName.count() - 1));
    const int superMetaTypeId = QMetaType::type(superPointerTypeName.toUtf8());
    const QMetaType superMetaType(superMetaTypeId);
    if (metaType.metaObject() == nullptr || superMetaType.metaObject() == nullptr)
        return false;
    return metaType.metaObject()->inherits(superMetaType.metaObject());
}
